package gestrues
{
	import flash.geom.Point;
	import flash.geom.Rectangle;

	/**
	 * 手势控制器
	 * @author Dont小鬼
	 * 2014-10-8
	 */
	public class DollarRecognizer
	{
		public static var NUM_POINTS:uint=64;
		public static var SQUARE_SIZE:Number=250;
		public static var HALF_DIAGNOAL:Number=0.5 * Math.sqrt(250 * 250 + 250 * 250);
		public static var ANGLE_RANGE:Number=45;
		public static var ANGLE_PRECISION:Number=2;
		public static var PHI:Number=0.5 * (-1 + Math.sqrt(5));

		protected var _templates:Array;

		public function DollarRecognizer(templates:Array=null)
		{
			_templates=templates || [];
		}

		public function addTemplate(t:DollarTemplate):void
		{
			_templates.push(t);
		}

		public function removeTemplate(t:DollarTemplate):void
		{
			for (var i:int=0; i < _templates.length; i++)
			{
				var tpl:DollarTemplate=_templates[i] as DollarTemplate;
				if (tpl.id == t.id)
				{
					_templates.splice(i, 1);
					i--;
				}
			}
		}

		public function removeAllTemplates():void
		{
			_templates=[];
		}

		public function recognize(points:Array):DollarResult
		{
			if (_templates.length == 0)
				return null;
			if (!points || points.length < 2)
				return null;
			var firstPoint:Point=new Point();
			firstPoint.x=points[0].x;
			firstPoint.y=points[0].y;
			points=resample(points.concat(), NUM_POINTS);
			points=rotateToZero(points);
			points=scaleToSquare(points, SQUARE_SIZE);
			points=translateToOrigin(points);
			var b:Number=Number.POSITIVE_INFINITY;
			var t:uint=0;
			var d:Number;
			for (var i:int=0; i < _templates.length; i++)
			{
				d=distanceAtBestAngle(points, _templates[i] as DollarTemplate, -ANGLE_RANGE,
					+ANGLE_RANGE, ANGLE_PRECISION);
				if (d < b)
				{
					b=d;
					t=i;
				}
			}
			var score:Number=1.0 - (b / HALF_DIAGNOAL);
			score=Math.max(0, Math.min(1, score));
			return new DollarResult(_templates[t], score, firstPoint);
		}

		public static function resample(points:Array, numPoints:uint):Array
		{
			var interval:Number=pathLength(points) / (numPoints - 1);
			var distance:Number=0;
			var newPoints:Array=[(points[0] as Point).clone()];
			var p1:Point;
			var p2:Point;
			var d:Number;
			var qx:Number;
			var qy:Number;
			var q:Point;
			for (var i:int=1; i < points.length; i++)
			{
				p1=points[i - 1] as Point;
				p2=points[i] as Point;
				d=Point.distance(p1, p2);
				if ((distance + d) >= interval)
				{
					qx=p1.x + ((interval - distance) / d) * (p2.x - p1.x);
					qy=p1.y + ((interval - distance) / d) * (p2.y - p1.y);
					q=new Point(qx, qy);
					newPoints.push(q);
					points.splice(i, 0, q);
					distance=0;
				}
				else
				{
					distance+=d;
				}
			}
			if (newPoints.length == numPoints - 1)
			{
				newPoints.push((points[points.length - 1] as Point).clone());
			}
			return newPoints;
		}

		public static function rotateToZero(points:Array):Array
		{
			var c:Point=centroid(points);
			var p:Point=points[0] as Point;
			var theta:Number=Math.atan2(c.y - p.y, c.x - p.x);
			return rotateBy(points, -theta);
		}

		public static function scaleToSquare(points:Array, size:Number):Array
		{
			var b:Rectangle=boundingBox(points);
			var newPoints:Array=[];
			var p:Point;
			var qx:Number;
			var qy:Number;
			for each (p in points)
			{
				qx=p.x * (size / b.width);
				qy=p.y * (size / b.height);
				newPoints.push(new Point(qx, qy));
			}
			return newPoints;
		}

		public static function translateToOrigin(points:Array):Array
		{
			var c:Point=centroid(points);
			var newPoints:Array=[];
			var p:Point;
			var qx:Number;
			var qy:Number;
			for each (p in points)
			{
				qx=p.x - c.x;
				qy=p.y - c.y;
				newPoints.push(new Point(qx, qy));
			}
			return newPoints;
		}

		/**
		 * 路径总距离
		 * @param points
		 * @return
		 */
		protected static function pathLength(points:Array):Number
		{
			var d:Number=0;
			for (var i:int=1; i < points.length; i++)
			{
				d+=Point.distance(points[i - 1] as Point, points[i] as Point);
			}
			return d;
		}

		protected static function centroid(points:Array):Point
		{
			var x:Number=0;
			var y:Number=0;
			var p:Point = null;
			for each (p in points)
			{
				x+=p.x;
				y+=p.y;
			}
			x/=points.length;
			y/=points.length;
			return new Point(x, y);
		}

		protected static function rotateBy(points:Array, theta:Number):Array
		{
			var c:Point=centroid(points);
			var cos:Number=Math.cos(theta);
			var sin:Number=Math.sin(theta);
			var newPoints:Array=[];
			var p:Point;
			var qx:Number;
			var qy:Number;
			for each (p in points)
			{
				qx=(p.x - c.x) * cos - (p.y - c.y) * sin + c.x;
				qy=(p.x - c.x) * sin + (p.y - c.y) * cos + c.y;
				newPoints.push(new Point(qx, qy));
			}
			return newPoints;
		}

		protected static function boundingBox(points:Array):Rectangle
		{
			var minX:Number=Number.POSITIVE_INFINITY;
			var maxX:Number=Number.NEGATIVE_INFINITY;
			var minY:Number=Number.POSITIVE_INFINITY;
			var maxY:Number=Number.NEGATIVE_INFINITY;
			var p:Point = null;
			for each (p in points)
			{
				if (p.x < minX)
					minX=p.x;
				if (p.x > maxX)
					maxX=p.x;
				if (p.y < minY)
					minY=p.y;
				if (p.y > maxY)
					maxY=p.y;
			}
			return new Rectangle(minX, minY, maxX - minX, maxY - minY);
		}

		protected function distanceAtBestAngle(points:Array, t:DollarTemplate, a:Number, b:Number, threshold:Number):Number
		{
			var x1:Number=PHI * a + (1 - PHI) * b;
			var f1:Number=distanceAtAngle(points, t, x1);
			var x2:Number=(1 - PHI) * a + PHI * b;
			var f2:Number=distanceAtAngle(points, t, x2);
			while (Math.abs(b - a) > threshold)
			{
				if (f1 < f2)
				{
					b=x2;
					x2=x1;
					f2=f1;
					x1=PHI * a + (1 - PHI) * b;
					f1=distanceAtAngle(points, t, x1);
				}
				else
				{
					a=x1;
					x1=x2;
					f1=f2;
					x2=(1 - PHI) * a + PHI * b;
					f2=distanceAtAngle(points, t, x2);
				}
			}
			return Math.min(f1, f2);
		}

		protected function distanceAtAngle(points:Array, t:DollarTemplate, theta:Number):Number
		{
			var newPoints:Array=rotateBy(points, theta);
			return pathDistance(newPoints, t.points);
		}

		protected static function pathDistance(pts1:Array, pts2:Array):Number
		{
			var d:Number=0;
			for (var i:int=0; i < pts1.length; i++)
			{
				d+=Point.distance(pts1[i] as Point, pts2[i] as Point);
			}
			return d / pts1.length;
		}

	}
}

